package text

import (
	"image"
	"strings"
)

const (
	selNone = iota
	selLeft
	selRight

	leftbrackets  = "{[(<"
	rightbrackets = "}])>"
	quotes        = "'`\""
)

type line struct {
	s     []rune
	img   *image.RGBA
	px    []int // maps column to X pixel
	dirty bool
}

type Address struct {
	Left, Right image.Point // left/right bounds of selection (left means comes-before)
	selection   int         // sweep direction (sel{Left,Right,None})
}

func (b *Buffer) NewAddress(left, right image.Point) Address {
	a := Address{Left: left, Right: right}
	if left != right {
		a.selection = selRight
	}
	return a
}

// define an ordering for points in our context
func before(p1, p2 image.Point) bool {
	return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X)
}

func (b *Buffer) selto(pt image.Point) {
	switch b.Dot.selection {
	case selNone:
		if before(pt, b.Dot.Left) {
			b.Dot.selection = selLeft
			b.Dot.Left = pt
		} else if pt.Eq(b.Dot.Left) {
			// no-op
		} else {
			b.Dot.selection = selRight
			b.Dot.Right = pt
		}
	case selLeft:
		if !before(pt, b.Dot.Right) && pt != b.Dot.Right {
			// the direction has reversed (e.g. sweep left and then down)
			b.Dot.Left = b.Dot.Right
			b.Dot.Right = pt
			b.Dot.selection = selRight
		} else {
			b.Dot.Left = pt
			if b.Dot.Left == b.Dot.Right {
				b.Dot.selection = selNone
			}
		}
	case selRight:
		if before(pt, b.Dot.Left) {
			b.Dot.Right = b.Dot.Left
			b.Dot.Left = pt
			b.Dot.selection = selLeft
		} else {
			b.Dot.Right = pt
			if b.Dot.Left == b.Dot.Right {
				b.Dot.selection = selNone
			}
		}
	}
}

func (b *Buffer) selall() {
	b.Dot.Left = image.ZP
	lastline := len(b.lines) - 1
	b.Dot.Right = image.Pt(len(b.lines[lastline].s), lastline)
	b.Dot.selection = selRight
}

// returns true if a selection was attempted, successfully or not
func (b *Buffer) seldelimited(delims1, delims2 string) bool {
	left, right := b.Dot.Left, b.Dot.Right
	line := b.lines[left.Y].s
	var delim int
	var next func() image.Point
	if left.X > 0 {
		if delim = strings.IndexRune(delims1, line[left.X-1]); delim != -1 {
			// scan from left delimiter
			next = func() image.Point {
				if right.X+1 > len(line) {
					right.Y++
					if right.Y < len(b.lines) {
						line = b.lines[right.Y].s
					}
					right.X = 0
				} else {
					right.X++
				}
				return right
			}
			b.Dot.selection = selRight
		}
	}
	if next == nil && left.X < len(line) {
		if delim = strings.IndexRune(delims2, line[left.X]); delim != -1 {
			// scan from right delimiter
			// swap delimiters so that delim1 refers to the first one we encountered
			tmp := delims1
			delims1 = delims2
			delims2 = tmp
			next = func() image.Point {
				if left.X-1 < 0 {
					left.Y--
					if left.Y >= 0 {
						line = b.lines[left.Y].s
					}
					left.X = len(line)
				} else {
					left.X--
				}
				return left
			}
			b.Dot.selection = selLeft
		}
	}
	if next == nil {
		return false
	}
	stack := 0
	for {
		p := next()
		if p.Y < 0 || p.Y >= len(b.lines) {
			b.Dot.selection = selNone
			return true
		} else if p.X >= len(b.lines[p.Y].s) {
			continue
		}
		c := b.lines[p.Y].s[p.X]
		if c == rune(delims2[delim]) && stack == 0 {
			b.Dot.Left, b.Dot.Right = left, right
			if b.Dot.selection == selLeft {
				b.Dot.Left.X++
			}
			return true
		} else if c == 0 {
			b.Dot.selection = selNone
			return true
		}
		if delims1 != delims2 && c == rune(delims1[delim]) {
			stack++
		}
		if delims1 != delims2 && c == rune(delims2[delim]) {
			stack--
		}
	}
}

// taken from plan9port's src/cmd/acme/util.c
func isalnum(c rune) bool {
	// Hard to get absolutely right.  Use what we know about ASCII
	// and assume anything above the Latin control characters is
	// potentially an alphanumeric.

	if c <= ' ' {
		return false
	}
	if 0x7F <= c && c <= 0xA0 {
		return false
	}
	if strings.IndexRune("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c) != -1 {
		return false
	}
	return true
}

// getword returns the address of the word at the text coordinates pt
func (b *Buffer) getword(pt image.Point) *Address {
	if len(b.lines[pt.Y].s) == 0 {
		return nil
	}
	if pt.X >= len(b.lines[pt.Y].s) {
		pt.X = len(b.lines[pt.Y].s) - 1
	}
	left, right := pt.X, pt.X
	for x := pt.X; x > 0 && isalnum(b.lines[pt.Y].s[x-1]); x-- {
		left--
	}
	if isalnum(b.lines[pt.Y].s[pt.X]) {
		for x := pt.X; x < len(b.lines[pt.Y].s) && isalnum(b.lines[pt.Y].s[x]); x++ {
			right++
		}
	}
	return &Address{Left: image.Pt(left, pt.Y), Right: image.Pt(right, pt.Y)}
}

func (b *Buffer) doubleclick(pt image.Point) {
	b.Dot.Left, b.Dot.Right = pt, pt
	line := b.lines[pt.Y].s

	// select bracketed text
	if b.seldelimited(leftbrackets, rightbrackets) {
		return
	}

	// select line
	if pt.X == len(line) || pt.X == 0 {
		b.Dot.Left.X = 0
		if pt.Y+1 < len(b.lines) {
			b.Dot.Right.Y++
			b.Dot.Right.X = 0
		} else {
			b.Dot.Right.X = len(line)
		}
		if b.Dot.Left != b.Dot.Right {
			b.Dot.selection = selRight
		}
		return
	}

	// select quoted text
	if b.seldelimited(quotes, quotes) {
		return
	}

	// select word
	for x := pt.X; x > 0 && isalnum(line[x-1]); x-- {
		b.Dot.Left.X--
	}

	// if we're on a non-alphanumeric, attempt to select only to the left.
	if isalnum(line[pt.X]) {
		for x := pt.X; x < len(line) && isalnum(line[x]); x++ {
			b.Dot.Right.X++
		}
	}

	if b.Dot.Left.X != b.Dot.Right.X {
		b.Dot.selection = selRight
	}
}

func (b *Buffer) gettext(pt1, pt2 image.Point) string {
	x1, y1, x2, y2 := pt1.X, pt1.Y, pt2.X, pt2.Y
	if y1 == y2 {
		return string(b.lines[y1].s[x1:x2])
	} else {
		sel := string(b.lines[y1].s[x1:]) + "\n"
		for i := y1 + 1; i < y2; i++ {
			sel += string(b.lines[i].s) + "\n"
		}
		sel += string(b.lines[y2].s[:x2])
		return sel
	}
}

func (b *Buffer) getsel() string {
	return b.gettext(b.Dot.Left, b.Dot.Right)
}

func (b *Buffer) deletesel() {
	if b.Dot.selection == selNone {
		return
	}

	x1, y1, x2, y2 := b.Dot.Left.X, b.Dot.Left.Y, b.Dot.Right.X, b.Dot.Right.Y
	b.lines[y1].dirty = true
	b.lines[y2].dirty = true

	line := b.lines[y1].s
	line = b.lines[y1].s[:x1]
	b.lines[y1].s = append(line, b.lines[y2].s[x2:]...)
	if y2 > y1 {
		b.lines = append(b.lines[:y1+1], b.lines[y2+1:]...)
	}

	if b.Dot.Left.Y*b.font.Height < b.scrollpt.Y {
		b.scrollpt.Y = b.Dot.Left.Y * b.font.Height
	}

	b.Dot.selection = selNone
	b.Dot.Right = b.Dot.Left
}

// resulting state is not a selection
func (b *Buffer) input(input rune) {
	b.deletesel()
	x, y := b.Dot.Left.X, b.Dot.Left.Y
	b.lines[y].dirty = true

	if x == len(b.lines[y].s) {
		b.lines[y].s = append(b.lines[y].s, input)
	} else {
		line := string(b.lines[y].s)
		line = line[:x] + string(input) + line[x:]
		b.lines[y].s = []rune(line)
	}
	b.Dot.Left.X++
	b.Dot.Right = b.Dot.Left
}

func (b *Buffer) snarf(s string) {
	if s == "" {
		return
	}
	b.ClipChan <- s
}

// paste replaces the current selection with the contents of the clipboard channel,
// and selects the resulting text.
func (b *Buffer) paste() {
	// signal a paste on the clipboard channel
	b.ClipChan <- ""
	input := <-b.ClipChan
	pt := b.load(input)
	b.selto(pt)
}

// load replaces the current selection with the contents of s. The cursor is left
// at the beginning of the new text, and the position at the end of the new text
// is returned.
func (b *Buffer) load(s string) image.Point {
	b.deletesel()
	x1, y1 := b.Dot.Left.X, b.Dot.Left.Y
	b.lines[y1].dirty = true

	inputlines := strings.Split(s, "\n")
	y2 := y1 + len(inputlines) - 1

	// save remainder as a string for immutability
	remainder := string(b.lines[y1].s[x1:])
	b.lines[y1].s = append(b.lines[y1].s[:x1], []rune(inputlines[0])...)

	// create new lines as needed
	if len(inputlines) > 1 {
		inputlines = inputlines[1:]
		newlines := make([]*line, len(inputlines))
		for i := 0; i < len(inputlines); i++ {
			newlines[i] = &line{
				s:     []rune(inputlines[i]),
				dirty: true,
			}
		}
		if y1+1 < len(b.lines) {
			newlines = append(newlines, b.lines[y1+1:]...)
		}
		b.lines = append(b.lines[:y1+1], newlines...)
	}

	b.lines[y2].s = append(b.lines[y2].s, []rune(remainder)...)

	var x2 int
	if y2 > y1 {
		x2 = len(inputlines[len(inputlines)-1])
	} else {
		x2 = x1 + len(s)
	}

	return image.Pt(x2, y2)
}
